// Copyright 2010 Jan Potoms. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


goog.provide('extension.Context');
goog.require('extension.Options');

goog.scope(function() {
  
  
  
  /**
   * The extension context. Provides functionality for extension wide events and
   * requests.
   * @private 
   * @constructor
   */
  extension.Context = function() {    
    
    // this.initializeRequestTimeout(); // may be causing bugs
    
  };
  
  goog.addSingletonGetter = function(ctor) {
  ctor.getInstance = function() {
    return ctor.instance_ || (ctor.instance_ = new ctor());
  };
};
  
  
  /**
   * Holds the handlers for when events are handled locally.
   * @private 
   * @type{Object}
   */
  Context.prototype.handlers = {};

  /**
   * Holds the local servers for requests.
   * @private 
   * @type{Object}
   */
  Context.prototype.localServers = {};

  /**
   * Holds the remote servers for requests.
   * @private 
   * @type{Object}
   */
  Context.prototype.remoteServers = {};

  /**
   * Timeout time for requests.
   * @private 
   * @type{number}
   */
  Context.prototype.REQUEST_TIMEOUT = 5000;



  
  
  /**
   * Dispatches an event to the extension context.
   * 
   * @param {string} event The type of event to dispatch.
   * @param {Object=} opt_data Data that is relevant to the event.
   */
  Context.prototype.dispatchEvent = function(event, opt_data) {
    
    // dispatch the event remote first.
    var msg = {
      'type': 'event',
      'eventType': event,
      'data': opt_data
    };
    
    // do nothing with the response, we don't care whether it is handled or not.
    chrome.extension.sendRequest(msg, function(response) { });
    
    // dispatch the event locally.
    var handlers = this.handlers[event] || [];
    for(var i = 0; i < handlers.length; i++) {
      handlers[i].localHandler(opt_data);
    }
    
  };
  
  
  
  
  
  /**
   * Listens for an event on the extension context.
   * 
   * @param {string} event The type of event to listen for.
   * @param {function(Object)} handler The function to execute when the event fires.
   */
  Context.prototype.addEventListener = function(event, handler) { 
    
    // Listen for events dispatched from other parts of the extension.
    var remoteHandler = function(request, sender, sendResponse) {
        
      // Send the response right away. The method that fired the event
      // doesn't care whether it is handled or not
      sendResponse();
      
      // filter the message for this type of event
      if(request['type'] === 'event' && request['eventType'] === event) {
        handler(request['data']);
      }
      
    }
    chrome.extension.onRequest.addListener(remoteHandler);
    
    this.handlers[event] = this.handlers[event] || [];
    this.handlers[event].push({
      localHandler: handler,
      remoteHandler: remoteHandler
    });
  };
  
  
  
  
  
  /**
   * Removes an eventhandler
   *
   * @param {string} event The type of event to remove the handler from. 
   * @param {function(Object)} handler The handler to remove.
   */
  Context.prototype.removeEventListener = function(event, handler) { 
    
    var handlers = this.handlers[event] || [];
    
    for(var i = 0; i < handlers.length; i++) {
      if(handlers[i].localHandler === handler) {
        
        // remove the remote eventlistener
        chrome.extension.onRequest.removeListener(handlers[i].remoteHandler);
        
        // remove the handler from the list
        handlers.splice(i, 1);
        
        // We removed the current one so step 1 back
        i -= 1;
        
      }
    }
  
  };
 
  
  
  
  
  /**
   * Sends a request across the extension.
   * 
   * @param {string} request The desired request.
   * @param {Object} data Some data to be sent along. 
   * @param {function(Object=)} onSuccess Handler when request is completed with
   * @param {function(Object=)=} opt_onFailure Handler when request failed
   */
  Context.prototype.request = function(request, data, onSuccess, opt_onFailure) {
    
    // handle request locally
    var localRequest = this.localServers[request];    
    if(localRequest) {
      
      localRequest(data, onSuccess, opt_onFailure);
      
      // request handled locally, no need to send it to the rest of the extension
      return; 
      
    }
    
    // handle request remote  
    var msg = {
      'type': 'request',
      'requestType': request,
      'data': data
    };
    
    chrome.extension.sendRequest(msg, function(response) {
      if(response['success']) {
        onSuccess(response['data']);
      } else {
        if (opt_onFailure) {
          opt_onFailure(response['error']);
        }
      }
    });
  
  };
  
  
  
  
  
  /**
   * Register a server to handle particular requests.
   * 
   * @param {string} requestType The type of request to send
   * @param {function(Object, function(Object), function(Object)=)} processRequest
   */
  Context.prototype.registerServer = function(requestType, processRequest) { 
  
    // add the server locally
    this.localServers[requestType] = processRequest;
    
    // remove an existing remote server
    var remoteServer = this.remoteServers[requestType];
    if (remoteServer) {
      chrome.extension.onRequest.removeListener(remoteServer);
    }
    
    // add a server to handle remote requests
    remoteServer = function(msg, sender, sendResponse) {
      if(msg['type'] === 'request' && msg['requestType'] === requestType) {
        
        processRequest(msg['data'],
          function (result) {
            sendResponse({
              'success': true,
              'data': result
            });            
          },
          function (error) {            
            sendResponse({
              'success': false,
              'data': error
            });            
          }
        );
        
      }
    }
    chrome.extension.onRequest.addListener(remoteServer);
    this.remoteServers[requestType] = remoteServer;
    
  };
  
  
  
  
  
  /**
   * Make sure every request is handled even in the case a request was sent to a 
   * non-existing server.
   * @private
   */
  Context.prototype.initializeRequestTimeout = function () {
    
    chrome.extension.onRequest.addListener(function (msg, sender, sendResponse) {
      if (msg['type'] === 'request') {
        
        setTimeout(function(){
          sendResponse({
            'success': false,
            'error': 'Timeout'
          });
        }, this.REQUEST_TIMEOUT);
        
      } else if (msg['type'] === 'event') {
        
        // Send the response right away. The method that fired the event
        // doesn't care whether it is handled or not
        sendResponse();
        
      }
    });
    
  };
  
  
  

  
  
  /**
   * Queries an option in the extension context.
   * 
   * @param {string} option The requested option.
   */
  Context.prototype.getOption = function (option) {
    
    var optionInfo = extension.Options[option];
    var value = window.localStorage[option];
    
    if (!optionInfo) {
      console.warn('Unknown option: \'' + option + '\'.');
    }
    
    if (optionInfo.validate && !optionInfo.validate(value)) {
      // invalid value
      console.warn('\'' + value + '\' is not valid for Option \'' + option + '\'.');
      return optionInfo.defaultValue;
    }
    
    return window.localStorage[option];
    
  };
  
  
  /**
   * Sets an option in the extension context.
   * 
   * @param {string} option The option to be set.
   * @param {*} value The value it must be set to.
   */
  Context.prototype.setOption = function (option, value) {
    
    var optionInfo = extension.Options[option];
    
    if (optionInfo) {
      if (optionInfo.readOnly) {
        // option is read-only
        console.error('Option \'' + option + '\' is read-only.');
        return;
      }
      
      if (optionInfo.validate && !optionInfo.validate(value)) {
        // invalid value
        console.error('\'' + value + '\' is not valid for Option \'' + option + '\'.');
        return;
      }
    } else {
      // unrecognized option
      console.warn('Unknown option: \'' + option + '\'.');
    }
    
    window.localStorage[option] = value;
    
    // TODO: dispatch setting changed event
    
  };
  
  
  
  Context.prototype.ensureDefaultOptions = function () {
    
    for(var option in extension.Options) {
      
      var optionInfo = extension.Options[option];
      var currentValue = window.localStorage[option];
      
      // self test, fail = programming error!
      if(optionInfo.validate && !optionInfo.validate(optionInfo.defaultValue)) {
        console.error('Option \'' + option + '\' has an invalid default value \'' + optionInfo.defaultValue + '\'.');
        continue;
      }
      
      if(currentValue !== undefined) {
        if(!optionInfo.validate || optionInfo.validate && optionInfo.validate(currentValue)) {
          // valid value present for this option
          continue
        }
      }
      
      // option not set or invalid -> set the default value
      window.localStorage[option] = optionInfo.defaultValue;
    }
        
  };
  
  
});